package search;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/*
 * Naive approach to implement similarity.
 * This approach doesn't assume any knowledge of Jaccard similarity or MinHash.
 * 
 *  Terms used in this implementation:
 *  1. Exact Match (or identical): when two users have exact set of attributes / interests e.g.
 *  intersection of their attributes is same as union of those.
 *  2. Partial Match (similar): when intersection of their attributes is a number greater than 
 *  zero but less than exact match. 
 * 
 * */
public class SimilarityFind {

	// API starts

	/*
	 * input a list of interests and output the users for both exact and partial match
	 * 
	 * UseCase: find the set of users who have interest running (ID 4) and cycling (ID 5)
	 * so that they get ads of a newly-launched sports brand
	 * 
	 * Three cases of match:
	 * 1. if (interestSet.size() == match), then return the set of users who have all the given 
	 * interests in interestSet. The users might have additional interests.
	 * 2. if (match >= 1 && match < interestSet.size()), then return the set of users who have
	 * at least 'match' number of interests from the given interestSet e.g. if match is 2 and interestSet
	 * has 5 interests, return the set of users who have at least two of the interestSet.
	 * 3. if match doesn't belong to above two cases, return all the users who have at least one interest
	 * in common. This result is same as the result for match == 1.
	 * 
	 * */
	public static String[] getUsers(Set<Integer> interestSet, int match){
		
		// get the interestMap
		Map<Integer, HashSet<String>> interestMap = InterestMapProcess.getInterestMap();
		
		Map<String, Integer> resultMap = new HashMap<String, Integer>();
		
		// this additional computation is to generate result based on the value of 'match'
		for(int i : interestSet){
			Set<String> userSet = interestMap.get(i);
			for(String user : userSet){
				Integer existingCount = resultMap.get(user);
				if(existingCount == null){
					resultMap.put(user, 1);
				}else{
					resultMap.put(user, ++existingCount);
				}
			}
		}
		
		// set of users to be returned
		Set<String> userSet =  new HashSet<String>();

		if(match >= 1 && match <= interestSet.size()){ // partial match
			Iterator<Map.Entry<String, Integer>> itr = resultMap.entrySet().iterator();
			while (itr.hasNext()) {
		        Map.Entry<String, Integer> pair = itr.next();
		        if(pair.getValue() >= match){
		        	userSet.add(pair.getKey());
		        }
		    }
		}else{ // return all the users
			userSet =  resultMap.keySet();
		}
		
		
		// return set of users as String array
		String[] users = userSet.toArray(new String[0]);
		Arrays.sort(users);
		return users;
	}
	

	// method: input a user and output the suggestion of users as potential friends both full and partial match
	public static String[] getUsers(User user, int match){
		String similarUsers[] = getUsers(user.getInterestSet(), match);
		Set<String> userSet = new HashSet<String>(Arrays.asList(similarUsers));
		userSet.remove(user.getUserID()); // remove given user from the result
		String[] users = userSet.toArray(new String[0]);
		Arrays.sort(users);
		return users; 
	}
	
	// API ends
} // class SimilarityFind ends


// This class is required to create a user
class User implements Comparable<User>{
	
	// list of interests
	// It'll store IDs of a user's interest e.g. reading, running etc. 
	private Set<Integer> interestSet = new HashSet<Integer>();
	
	// unique user ID
	private final String userID;
	
	// constructor
	User(String userID, Set<Integer> interestSet){
		
		// throw error in case userId is null or its a blank string
		// create empty set when interestSet is null 
		
		this.userID = userID;
		this.interestSet = interestSet;
		//Map<Integer, HashSet<String>> interestMap = InterestMapProcess.getInterestMap();
		InterestMapProcess.modifyInterestMap(userID, interestSet, true);
	}
	
	public String getUserID(){
		return userID;
	}

	public Set<Integer> getInterestSet(){
		return interestSet;
	}	
	
	@Override
	public String toString() {
		return userID;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((userID == null) ? 0 : userID.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		User other = (User) obj;
		if (userID == null) {
			if (other.userID != null)
				return false;
		} else if (!userID.equals(other.userID))
			return false;
		return true;
	}
	
	public int compareTo(User anotherUser) {
		return userID.compareTo(anotherUser.getUserID());
	}
	
} // class User ends


/*
 * This class is responsible for maintaining key-value pair where key is an Integer representing a particular
 * hobby or interest uniquely and, value is a set of users who follow a particular hobby.
 * 
 * */
class InterestMapProcess{

	// interestMap having a set of users for an interest
	private static Map<Integer, HashSet<String>> interestMap = new HashMap<Integer, HashSet<String>>();	

	// private constructor
	private InterestMapProcess(){
	}
	
	// return the only instance of interestMap
	public static Map<Integer, HashSet<String>> getInterestMap(){
		return interestMap;
	}
	
	// add or remove a set of interests for the given user
	public static boolean modifyInterestMap(String userID, Set<Integer> interestSet, boolean addOrRemove){
		if(addOrRemove == true){ // a set of interests to be added for the given user 
			for(int interestID : interestSet){
				HashSet<String> userSet = interestMap.get(interestID);
				if(userSet != null){
					userSet.add(userID);
					interestMap.put(interestID, userSet);
				}else{
					userSet = new HashSet<String>();
					userSet.add(userID);
					interestMap.put(interestID, userSet);
				}
			}
		}else{ // a set of interests to be removed for the given user
			for(int interestID : interestSet){
				HashSet<String> userSet = interestMap.get(interestID);
				if(userSet != null && !userSet.isEmpty()){
					userSet.remove(userID);
					interestMap.put(interestID, userSet);
				}
			}
		}
		return true;
	}
	
} // class InterestMapProcess ends